package com.flute.framework.search;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ComplexExplanation;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.BooleanClause.Occur;

public class FluteBooleanWeight extends Weight {

	private static final long serialVersionUID = -942268504863333947L;

	/** The Similarity implementation. */
	protected Similarity similarity;

	protected ArrayList<Weight> weights;
	private BooleanQuery query;

	public FluteBooleanWeight(BooleanQuery query, Searcher seracher)
			throws IOException {
		this.query = query;
		this.similarity = new LayerSimilarity();
		weights = new ArrayList<Weight>(query.clauses().size());
		for (int i = 0; i < query.getClauses().length; i++) {
			weights.add(query.getClauses()[i].getQuery().createWeight(seracher));
		}
	}

	@Override
	public Query getQuery() {
		return query;
	}

	@Override
	public float getValue() {
		return query.getBoost();
	}

	@Override
	public float sumOfSquaredWeights() throws IOException {
		float sum = 0.0f;
		for (int i = 0; i < weights.size(); i++) {
			// call sumOfSquaredWeights for all clauses in case of side effects
			float s = weights.get(i).sumOfSquaredWeights(); // sum sub weights
			if (!query.getClauses()[i].isProhibited())
				// only add to sum for non-prohibited clauses
				sum += s;
		}

		sum *= query.getBoost() * query.getBoost(); // boost each sub-weight

		return sum;
	}

	@Override
	public void normalize(float norm) {
		norm *= query.getBoost(); // incorporate boost
		for (Weight w : weights) {
			// normalize all clauses, (even if prohibited in case of side
			// affects)
			w.normalize(norm);
		}
	}

	@Override
	public Explanation explain(IndexReader reader, int doc) throws IOException {
		final int minShouldMatch = query.getMinimumNumberShouldMatch();
		ComplexExplanation sumExpl = new ComplexExplanation();
		sumExpl.setDescription("sum of:");
		int coord = 0;
		int maxCoord = 0;
		float sum = 0.0f;
		boolean fail = false;
		int shouldMatchCount = 0;
		Iterator<BooleanClause> cIter = query.iterator();
		for (Iterator<Weight> wIter = weights.iterator(); wIter.hasNext();) {
			Weight w = wIter.next();
			BooleanClause c = cIter.next();
			if (w.scorer(reader, true, true) == null) {
				continue;
			}
			Explanation e = w.explain(reader, doc);
			if (!c.isProhibited())
				maxCoord++;
			if (e.isMatch()) {
				if (!c.isProhibited()) {
					sumExpl.addDetail(e);
					sum += e.getValue();
					coord++;
				} else {
					Explanation r = new Explanation(0.0f,
							"match on prohibited clause ("
									+ c.getQuery().toString() + ")");
					r.addDetail(e);
					sumExpl.addDetail(r);
					fail = true;
				}
				if (c.getOccur() == Occur.SHOULD)
					shouldMatchCount++;
			} else if (c.isRequired()) {
				Explanation r = new Explanation(0.0f,
						"no match on required clause ("
								+ c.getQuery().toString() + ")");
				r.addDetail(e);
				sumExpl.addDetail(r);
				fail = true;
			}
		}
		if (fail) {
			sumExpl.setMatch(Boolean.FALSE);
			sumExpl.setValue(0.0f);
			sumExpl.setDescription("Failure to meet condition(s) of required/prohibited clause(s)");
			return sumExpl;
		} else if (shouldMatchCount < minShouldMatch) {
			sumExpl.setMatch(Boolean.FALSE);
			sumExpl.setValue(0.0f);
			sumExpl.setDescription("Failure to match minimum number "
					+ "of optional clauses: " + minShouldMatch);
			return sumExpl;
		}

		sumExpl.setMatch(0 < coord ? Boolean.TRUE : Boolean.FALSE);
		sumExpl.setValue(sum);

		float coordFactor = similarity.coord(coord, maxCoord);
		if (coordFactor == 1.0f) // coord is no-op
			return sumExpl; // eliminate wrapper
		else {
			ComplexExplanation result = new ComplexExplanation(
					sumExpl.isMatch(), sum * coordFactor, "product of:");
			result.addDetail(sumExpl);
			result.addDetail(new Explanation(coordFactor, "coord(" + coord
					+ "/" + maxCoord + ")"));
			return result;
		}
	}

	@Override
	public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder,
			boolean topScorer) throws IOException {
		List<Scorer> required = new ArrayList<Scorer>();
		List<Scorer> prohibited = new ArrayList<Scorer>();
		List<Scorer> optional = new ArrayList<Scorer>();
		Iterator<BooleanClause> cIter = query.iterator();
		for (Weight w : weights) {
			BooleanClause c = cIter.next();
			Scorer subScorer = w.scorer(reader, true, false);
			if (subScorer == null) {
				if (c.isRequired()) {
					return null;
				}
			} else if (c.isRequired()) {
				required.add(subScorer);
			} else if (c.isProhibited()) {
				prohibited.add(subScorer);
			} else {
				optional.add(subScorer);
			}
		}

		if (required.size() == 0 && optional.size() == 0) {
			// no required and optional clauses.
			return null;
		}

		// Return a BooleanScorer2
		optional.addAll(required);
		optional.addAll(prohibited);
		return new ConjunctionScorer(similarity, optional);
	}

	@Override
	public boolean scoresDocsOutOfOrder() {
		int numProhibited = 0;
		for (BooleanClause c : query.getClauses()) {
			if (c.isRequired()) {
				return false; // BS2 (in-order) will be used by scorer()
			} else if (c.isProhibited()) {
				++numProhibited;
			}
		}

		if (numProhibited > 32) { // cannot use BS
			return false;
		}

		// scorer() will return an out-of-order scorer if requested.
		return true;
	}

}
